package io.cici.cc.mybatis.lite.reflection;

import io.cici.cc.mybatis.lite.reflection.invoker.GetFieldInvoker;
import io.cici.cc.mybatis.lite.reflection.invoker.Invoker;
import io.cici.cc.mybatis.lite.reflection.invoker.MethodInvoker;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;

public class ClassMetaData {

    private final ReflectorFactory reflectorFactory;
    private final Reflector reflector;

    private ClassMetaData(ReflectorFactory reflectorFactory, Class<?> type) {
        this.reflectorFactory = reflectorFactory;
        this.reflector = reflectorFactory.getReflector(type);
    }

    public static ClassMetaData build(ReflectorFactory reflectorFactory, Class<?> type) {
        return new ClassMetaData(reflectorFactory, type);
    }

    public ClassMetaData metaClassForProperty(String name) {
        Class<?> aClass = reflector.getGetterType(name);
        return ClassMetaData.build(reflectorFactory, aClass);
    }

    public Class<?> getSetterType(String name) {
        PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name);
        if (propertyTokenizer.hasNext()) {
            ClassMetaData classMetaData = metaClassForProperty(propertyTokenizer.getName());
            return classMetaData.getSetterType(propertyTokenizer.getNext());
        }
        return reflector.getSetterType(propertyTokenizer.getName());
    }

    public Class<?> getGetterType(String name) {
        PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name);
        if (propertyTokenizer.hasNext()) {
            ClassMetaData classMetaData = metaClassForProperty(propertyTokenizer);
            return classMetaData.getGetterType(propertyTokenizer.getNext());
        }
        //Resolve the type inside a Collection Object
        return getGetterType(propertyTokenizer);
    }

    private ClassMetaData metaClassForProperty(PropertyTokenizer propertyTokenizer) {
        Class<?> propType = getGetterType(propertyTokenizer);
        return ClassMetaData.build(reflectorFactory, propType);
    }

    private Class<?> getGetterType(PropertyTokenizer propertyTokenizer) {
        Class<?> type = reflector.getGetterType(propertyTokenizer.getName());
        if (propertyTokenizer.getIndex() != null && Collection.class.isAssignableFrom(type)) {
            Type returnType = getGenericGetterType(propertyTokenizer.getName());
            if (returnType instanceof ParameterizedType) {
                Type[] actualTypeArguments = ((ParameterizedType) returnType).getActualTypeArguments();
                if (actualTypeArguments != null && actualTypeArguments.length == 1) {
                    returnType = actualTypeArguments[0];
                    if (returnType instanceof Class) {
                        type = (Class<?>) returnType;
                    } else if (returnType instanceof ParameterizedType) {
                        type = (Class<?>) ((ParameterizedType) returnType).getRawType();
                    }
                }
            }
        }
        return type;
    }

    private Type getGenericGetterType(String propertyName) {
        try {
            Invoker invoker = reflector.getGetInvoker(propertyName);
            if (invoker instanceof MethodInvoker) {
                Field declaredMethod = MethodInvoker.class.getDeclaredField("method");
                declaredMethod.setAccessible(true);
                Method method = (Method) declaredMethod.get(invoker);
                return TypeParameterResolver.resolveReturnType(reflector.getClazz(), method);
            }
            if (invoker instanceof GetFieldInvoker) {
                Field declaredField = GetFieldInvoker.class.getDeclaredField("field");
                declaredField.setAccessible(true);
                Field field = (Field) declaredField.get(invoker);
                return TypeParameterResolver.resolveFieldType(field, reflector.getClazz());
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            // Ignored
        }
        return null;
    }

    public boolean hasSetter(String name) {
        PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name);
        if (!propertyTokenizer.hasNext()) {
            return reflector.hasSetter(propertyTokenizer.getName());
        }
        if (reflector.hasSetter(propertyTokenizer.getName())) {
            ClassMetaData classMetaData = metaClassForProperty(propertyTokenizer.getName());
            return classMetaData.hasSetter(propertyTokenizer.getNext());
        }
        return false;
    }

    public boolean hasGetter(String name) {
        PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name);
        if (!propertyTokenizer.hasNext()) {
            return reflector.hasGetter(propertyTokenizer.getName());
        }
        if (reflector.hasGetter(propertyTokenizer.getName())) {
            ClassMetaData classMetaData = metaClassForProperty(propertyTokenizer);
            return classMetaData.hasGetter(propertyTokenizer.getNext());
        }
        return false;
    }

    public Invoker getGetInvoker(String name) {
        return reflector.getGetInvoker(name);
    }

    public Invoker getSetInvoker(String name) {
        return reflector.getSetInvoker(name);
    }

    public boolean hasDefaultConstructor() {
        return reflector.hasDefaultConstructor();
    }

}
